import {
  Scene,
  PerspectiveCamera,
  AxesHelper,
  WebGLRenderer,
  Camera,
  SRGBColorSpace,
  AmbientLight,
  Raycaster,
  Vector2
} from 'three';
import * as THREE from 'three';
import mitt, { type Emitter } from 'mitt';
import Events from './Events';
import { throttle } from 'lodash-es';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import Stats from 'three/examples/jsm/libs/stats.module';
import SkyBoxs from '../SkyBoxs';

export type Animate = {
  fun: (arg: any) => any,
  content: any,
}

// 封装场景 Viewer 类
export default class Viewer {
  public id: string;
  public viewerDom!: HTMLElement;
  public scene!: Scene;
  public camera!: PerspectiveCamera;
  public renderer!: WebGLRenderer;
  public controls!: OrbitControls;
  public skyboxs!: SkyBoxs;
  public animateEventList: any[] = [];
  public statsControls!: Stats;
  public raycaster!: Raycaster;
  public mouse!: Vector2;
  public emitter!: Emitter<any>;
  public mouseEvent: MouseEvent | undefined;
  public raycasterObjects: THREE.Object3D[] = [];
  public isDestroy: boolean = false;

  constructor(id: string) {
    this.id = id;
    this.initViewer();
  }

  /**坐标轴辅助 */
  public addAxis() {
    // 创建一个 AxesHelper 实例，并指定轴线的长度为 1000。AxesHelper 是 Three.js 中的工具类，用于创建表示坐标轴的辅助对象。
    const axis = new AxesHelper(1000);
    this.scene?.add(axis);
  }

  public addAnimate(animate: Animate) {
    // addAnimate(animate: Animate) 方法接受一个 animate 参数，该参数是一个动画对象（可能是一个函数、类实例或其他类型的对象）。
    //  将传入的动画对象 animate 添加到 animateEventList 数组中。animateEventList 是一个存储动画事件的数组，用于管理场景中的各种动画效果。
    this.animateEventList.push(animate);
  }


  /**
  * 添加性能状态监测
  */
  // 通过调用这个方法，可以在页面中实时显示应用程序的性能统计信息，
  // 例如帧率（FPS）、内存占用等指标。同时，将性能统计信息添加到动画循环中，保证性能统计数据能够及时更新并在页面上展示出来。
  // 这有助于开发人员监控应用程序的性能表现并进行优化。
  public addStats() {
    // 首先检查是否已经存在性能统计对象 this.statsControls，如果不存在，则创建一个新的 Stats 实例。
    if (!this.statsControls) this.statsControls = new Stats();
    // 将性能统计的 DOM 元素的定位样式设置为绝对定位，以便在页面上正确显示性能统计信息。
    this.statsControls.dom.style.position = 'absolute';
    // 将性能统计的 DOM 元素添加到指定的父元素 this.viewerDom 中，将性能统计信息显示在页面上。
    this.viewerDom.appendChild(this.statsControls.dom);
    // 调用 this.addAnimate() 方法，将性能统计的更新函数 this.statsUpdate 添加到动画循环中。这样可以确保在每一帧更新性能统计信息。
    // 添加到动画
    this.addAnimate({
      fun: this.statsUpdate,
      content: this.statsControls,
    });
  }

  /**注册鼠标事件监听 */
  // 通过调用这个方法，可以初始化射线拾取功能，并在鼠标点击、双击和移动事件发生时进行相应的射线拾取操作，以实现交互式的场景控制和对象选择功能。
  public initRaycaster() {
    // 创建一个 Raycaster 实例并将其赋值给类成员变量 this.raycaster。Raycaster 是 Three.js 中用于进行射线拾取的工具类
    this.raycaster = new Raycaster();
    // 定义了一个内部函数 initRaycasterEvent，该函数接受一个事件名 eventName 作为参数，并在指定的 DOM 元素上添加事件监听器。
    const initRaycasterEvent: Function = (eventName: keyof HTMLElementEventMap): void => {
      // 在 initRaycasterEvent 函数中，使用了 throttle 函数来限制事件处理函数的执行频率，以提高性能。
      const funWrap = throttle(
        (event: any) => {
          // 在事件处理函数中，根据鼠标事件的坐标计算了 mouse.x 和 mouse.y 的值，用于确定射线的起点位置。
          this.mouseEvent = event;
          this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
          this.mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
          // @ts-expect-error
          // 调用 this.getRaycasterIntersectObjects() 方法获取射线与场景中物体的交点，并通过事件触发器（emitter）将结果传递给相应的事件处理函数
          this.emitter.emit(Events[eventName].raycaster, this.getRaycasterIntersectObjects());
        },
        50
      );
      this.viewerDom.addEventListener(eventName, funWrap, false);
    };
    // 最后，分别对 'click'、'dblclick' 和 'mousemove' 事件调用 initRaycasterEvent 函数，初始化这些事件的射线拾取功能。
    initRaycasterEvent('click');
    initRaycasterEvent('dblclick');
    initRaycasterEvent('mousemove');
  }

  /**销毁场景 */
  public destroy() {
    // 使用 traverse() 方法遍历场景中的每个子对象。
    this.scene.traverse((child: any) => {
      if (child.material) {
        // 对于每个子对象，首先判断是否存在材质（material），如果存在则调用 dispose() 方法来释放材质占用的资源。
        child.material.dispose();
      }
      if (child.geometry) {
        // 然后判断是否存在几何体（geometry），如果存在则调用 dispose() 方法来释放几何体占用的资源
        child.geometry.dispose();
      }
      // 将子对象设置为 null，以便垃圾回收机制可以回收该对象所占用的内存。
      child = null;
    });
    // 调用 forceContextLoss() 方法来强制释放 WebGL 上下文。
    this.renderer.forceContextLoss();
    // 调用 dispose() 方法来释放渲染器（renderer）占用的资源。
    this.renderer.dispose();
    // 调用 clear() 方法来清空场景中的所有对象。
    this.scene.clear();
    // 最后将 isDestroy 标志设置为 true，表示场景已被销毁。
    this.isDestroy = true;
  }

  // 更新性能统计（Performance Stats）的方法
  private statsUpdate(statsControls: any) {
    // 调用 statsControls 对象的 update() 方法来更新性能统计信息。通常，性能统计工具（如 Stats.js）会提供一个 update() 方法，用于更新性能数据并在屏幕上显示最新的性能信息
    // 通过调用这个方法，可以在每一帧更新性能统计信息，以便实时监测应用程序的性能表现，例如帧率（FPS）、内存占用等指标。这有助于开发人员优化应用程序的性能并进行调试。
    statsControls.update();
  }

  private initViewer() {
    this.emitter = mitt();
    // 初始化渲染器
    this.initRenderer();
    // 初始化场景
    this.initScene();
    // 初始化灯光
    this.initLight();
    // 初始化相机
    this.initCamera();
    // 初始化摄像机控制器
    this.initControl();
    // 初始化天空盒
    this.initSkybox();

    this.raycaster = new Raycaster();
    this.mouse = new Vector2();

    const animate = () => {
      if (this.isDestroy) return;
      requestAnimationFrame(animate);

      this.updateDom();
      this.readerDom();

      // 全局的公共动画函数，添加函数可同步执行
      this.animateEventList.forEach(event => {
        // event.fun && event.content && event.fun(event.content);
        if (event.fun && event.content) {
          event.fun(event.content);
        }
      });
    };

    animate();
  }

  // 初始化场景
  private initScene() {
    this.scene = new Scene();
  }

  // 初始化相机
  private initCamera() {
    // 渲染相机
    this.camera = new PerspectiveCamera(25, window.innerWidth / window.innerHeight, 1, 2000);
    //设置相机位置
    this.camera.position.set(4, 2, -3);
    //设置相机方向
    this.camera.lookAt(0, 0, 0);
  }

  // 初始化渲染器
  private initRenderer() {
    // 获取画布dom
    this.viewerDom = document.getElementById(this.id) as HTMLElement;
    // 初始化渲染器
    this.renderer = new WebGLRenderer({
      // 启用对数深度缓冲（logarithmic depth buffer）
      logarithmicDepthBuffer: true,
      antialias: true, // true/false表示是否开启反锯齿
      alpha: true, // true/false 表示是否可以设置背景色透明
      precision: 'mediump', // highp/mediump/lowp 表示着色精度选择
      premultipliedAlpha: true, // true/false 表示是否可以设置像素深度（用来度量图像的分辨率）
      // preserveDrawingBuffer: false, // true/false 表示是否保存绘图缓冲
      // physicallyCorrectLights: true, // true/false 表示是否开启物理光照
    });
    // 这行代码的作用是清除渲染器的深度缓冲区。在 Three.js 中，深度缓冲区用于存储每个像素的深度信息，以便在渲染时进行深度测试，确保物体按正确的顺序绘制，避免遮挡关系错误。
    this.renderer.clearDepth();
    // 启用渲染器的阴影映射功能（shadowMap.enabled = true），这样可以在场景中生成和渲染阴影
    this.renderer.shadowMap.enabled = true;
    // 设置渲染器的输出颜色空间为 SRGBColorSpace，这将使材质显示更亮，同时也会影响环境贴图的效果。
    this.renderer.outputColorSpace = SRGBColorSpace; // 可以看到更亮的材质，同时这也影响到环境贴图。
    this.viewerDom.appendChild(this.renderer.domElement);
  }

  // 初始化摄像机控制器
  private initControl() {
    // 使用 OrbitControls 类创建了一个新的摄像机控制器实例，并将相机对象（this.camera）和渲染器的 dom 元素作为参数传入。
    this.controls = new OrbitControls(
      this.camera as Camera,
      this.renderer?.domElement
    );
    // 将摄像机控制器的 enableDamping 属性设置为 false，表示不启用阻尼效果，即摄像机不会因惯性而继续移动。
    this.controls.enableDamping = false;
    // 将摄像机控制器的 screenSpacePanning 属性设置为 false，这表示在平移时摄像机的移动方式，这里是控制不允许上下移动。
    this.controls.screenSpacePanning = false; // 定义平移时如何平移相机的位置 控制不上下移动
    // 设置摄像机控制器的最小和最大缩放距离，minDistance 和 maxDistance 分别表示摄像机与目标的最小和最大距离。
    this.controls.minDistance = 2;
    this.controls.maxDistance = 1000;
    // 给摄像机控制器添加了一个 'change' 事件监听器，在摄像机控制器状态发生改变时执行回调函数，在回调函数内重新渲染场景。
    this.controls.addEventListener('change', () => {
      this.renderer.render(this.scene, this.camera);
    });
  }

  // 初始化天空盒（Skybox）的方法
  private initSkybox() {
    if (!this.skyboxs) this.skyboxs = new SkyBoxs(this);
    // 调用 SkyBoxs 实例的 addSkybox('night') 方法，该方法用于添加一个名为 'night' 的天空盒。具体来说，这个方法会加载 'night' 天空盒的纹理贴图并应用到场景中，以营造夜晚的环境效果。
    this.skyboxs.addSkybox('night');
    // 调用 SkyBoxs 实例的 addFog() 方法，该方法用于向场景中添加雾效果。雾化效果可以增加场景的立体感和氛围，使远处物体逐渐消失在雾中，营造出更加真实的视觉效果。
    this.skyboxs.addFog();
  }

  // 初始化灯光
  private initLight() {
    // 创建了一个环境光（AmbientLight）对象，并设置光的颜色为白色（0xffffff），光的强度为 0.6。环境光是一种均匀照射场景的光源，用于模拟整体的光照效果。
    const ambient = new AmbientLight(0xffffff, 0.6);
    // 将环境光添加到场景中，以使整个场景都受到环境光的照射。
    this.scene.add(ambient);
    // 创建了一个定向光（DirectionalLight）对象，光的颜色为白色（0xffffff）。
    const light = new THREE.DirectionalLight(0xffffff);
    // 设置定向光的位置为 (0, 200, 100)，表示光源的位置在场景中的 (x:0, y:200, z:100) 处。
    light.position.set(0, 200, 100);
    // 将定向光设置为可以产生阴影（castShadow = true）
    light.castShadow = true;
    // 配置定向光的阴影相机（shadow.camera）的参数，包括视锥体的上下左右边界、近平面和远平面的距离，这些参数将影响阴影的投射范围和清晰度
    light.shadow.camera.top = 180;
    light.shadow.camera.bottom = -100;
    light.shadow.camera.left = -120;
    light.shadow.camera.right = 400;
    light.shadow.camera.near = 0.1;
    light.shadow.camera.far = 400;
    // 设置阴影贴图的大小为 1024x1024，通过调整 mapSize 属性可以使阴影显示更清晰。
    // 设置mapSize属性可以使阴影更清晰，不那么模糊
    light.shadow.mapSize.set(1024, 1024);

    this.scene.add(light);
  }

  // 渲染dom
  private readerDom() {
    this.renderer?.render(this.scene as Scene, this.camera as Camera);
  }

  // 更新dom
  private updateDom() {
    this.controls.update();
    // 更新参数
    // 摄像机视锥体的长宽比，通常是使用画布的宽/画布的高
    this.camera.aspect = this.viewerDom.clientWidth / this.viewerDom.clientHeight;
    //  更新摄像机投影矩阵。在任何参数被改变以后必须被调用,来使得这些改变生效
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(this.viewerDom.clientWidth, this.viewerDom.clientHeight);
    // 设置设备像素比
    this.renderer.setPixelRatio(window.devicePixelRatio);
  }

  /**自定义鼠标事件触发的范围，给定一个模型组，对给定的模型组鼠标事件才生效 */
  public setRaycasterObjects(objList: THREE.Object3D[]): void {
    this.raycasterObjects = objList;
  }

  // 用于获取射线与指定物体（Objects）相交的交点（Intersections）
  // 通过调用这个方法，可以根据当前鼠标位置和相机信息，计算射线与指定物体数组中的物体相交的交点，并返回交点集合。这个方法通常在射线拾取操作中被调用，用于获取用户点击或悬停的物体信息，以便进行后续的交互处理。
  private getRaycasterIntersectObjects(): THREE.Intersection[] {
    // 首先检查是否存在要进行射线拾取的物体数组 this.raycasterObjects，如果数组为空，则直接返回空数组。
    if (!this.raycasterObjects.length) return [];
    // 使用 this.raycaster.setFromCamera(this.mouse, this.camera) 方法来设置射线的起点和方向。这里使用了鼠标位置 this.mouse 和相机 this.camera 的信息
    this.raycaster.setFromCamera(this.mouse, this.camera);
    // 调用 this.raycaster.intersectObjects(this.raycasterObjects, true) 方法来获取射线与指定物体数组 this.raycasterObjects 相交的交点集合。第二个参数 true 表示递归地检查物体的所有子物体。
    return this.raycaster.intersectObjects(this.raycasterObjects, true);
  }
}
